package com.vpaliy.melophile.ui.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import com.vpaliy.melophile.R;

import de.hdodenhof.circleimageview.CircleImageView;


public class CircularSeekBar extends CircleImageView {

  // Default values
  private static final float DEFAULT_CIRCLE_X_RADIUS = 30f;
  private static final float DEFAULT_CIRCLE_Y_RADIUS = 30f;
  private static final float DEFAULT_POINTER_RADIUS = 7f;
  private static final float DEFAULT_POINTER_HALO_WIDTH = 6f;
  private static final float DEFAULT_POINTER_HALO_BORDER_WIDTH = 2f;
  private static final float DEFAULT_CIRCLE_STROKE_WIDTH = 5f;
  private static final float DEFAULT_START_ANGLE = 270f; // Geometric (clockwise, relative to 3 o'clock)
  private static final float DEFAULT_END_ANGLE = 270f; // Geometric (clockwise, relative to 3 o'clock)
  private static final int DEFAULT_MAX = 100;
  private static final int DEFAULT_PROGRESS = 0;
  private static final int DEFAULT_CIRCLE_COLOR = Color.DKGRAY;
  private static final int DEFAULT_CIRCLE_PROGRESS_COLOR = Color.argb(235, 74, 138, 255);
  private static final int DEFAULT_POINTER_COLOR = Color.argb(235, 74, 138, 255);
  private static final int DEFAULT_POINTER_HALO_COLOR = Color.argb(135, 74, 138, 255);
  private static final int DEFAULT_POINTER_HALO_COLOR_ONTOUCH = Color.argb(135, 74, 138, 255);
  private static final int DEFAULT_CIRCLE_FILL_COLOR = Color.TRANSPARENT;
  private static final int DEFAULT_POINTER_ALPHA = 135;
  private static final int DEFAULT_POINTER_ALPHA_ONTOUCH = 100;
  private static final boolean DEFAULT_USE_CUSTOM_RADII = false;
  private static final boolean DEFAULT_MAINTAIN_EQUAL_CIRCLE = true;
  private static final boolean DEFAULT_MOVE_OUTSIDE_CIRCLE = false;
  private static final boolean DEFAULT_LOCK_ENABLED = true;
  /**
   * Used to scale the dp units to pixels
   */
  private final float DPTOPX_SCALE = getResources().getDisplayMetrics().density;
  /**
   * Minimum touch target size in DP. 48dp is the Android design recommendation
   */
  private final float MIN_TOUCH_TARGET_DP = 48;
  /**
   * {@code Paint} instance used to draw the inactive circle.
   */
  private Paint mCirclePaint;

  /**
   * {@code Paint} instance used to draw the circle fill.
   */
  private Paint mCircleFillPaint;

  /**
   * {@code Paint} instance used to draw the active circle (represents progress).
   */
  private Paint mCircleProgressPaint;

  /**
   * {@code Paint} instance used to draw the glow from the active circle.
   */
  private Paint mCircleProgressGlowPaint;

  /**
   * {@code Paint} instance used to draw the center of the pointer.
   * Note: This is broken on 4.0+, as BlurMasks do not work with hardware acceleration.
   */
  private Paint mPointerPaint;

  /**
   * {@code Paint} instance used to draw the halo of the pointer.
   * Note: The halo is the part that changes transparency.
   */
  private Paint mPointerHaloPaint;

  /**
   * {@code Paint} instance used to draw the border of the pointer, outside of the halo.
   */
  private Paint mPointerHaloBorderPaint;

  /**
   * The width of the circle (in pixels).
   */
  private float mCircleStrokeWidth;

  /**
   * The X radius of the circle (in pixels).
   */
  private float mCircleXRadius;

  /**
   * The Y radius of the circle (in pixels).
   */
  private float mCircleYRadius;

  /**
   * The radius of the pointer (in pixels).
   */
  private float mPointerRadius;

  /**
   * The width of the pointer halo (in pixels).
   */
  private float mPointerHaloWidth;

  /**
   * The width of the pointer halo border (in pixels).
   */
  private float mPointerHaloBorderWidth;

  /**
   * Start angle of the CircularSeekBar.
   * Note: If mStartAngle and mEndAngle are set to the same angle, 0.1 is subtracted
   * from the mEndAngle to make the circle function properly.
   */
  private float mStartAngle;

  /**
   * End angle of the CircularSeekBar.
   * Note: If mStartAngle and mEndAngle are set to the same angle, 0.1 is subtracted
   * from the mEndAngle to make the circle function properly.
   */
  private float mEndAngle;

  /**
   * {@code RectF} that represents the circle (or ellipse) of the seekbar.
   */
  private RectF mCircleRectF = new RectF();

  /**
   * Holds the color value for {@code mPointerPaint} before the {@code Paint} instance is created.
   */
  private int mPointerColor = DEFAULT_POINTER_COLOR;

  /**
   * Holds the color value for {@code mPointerHaloPaint} before the {@code Paint} instance is created.
   */
  private int mPointerHaloColor = DEFAULT_POINTER_HALO_COLOR;

  /**
   * Holds the color value for {@code mPointerHaloPaint} before the {@code Paint} instance is created.
   */
  private int mPointerHaloColorOnTouch = DEFAULT_POINTER_HALO_COLOR_ONTOUCH;

  /**
   * Holds the color value for {@code mCirclePaint} before the {@code Paint} instance is created.
   */
  private int mCircleColor = DEFAULT_CIRCLE_COLOR;

  /**
   * Holds the color value for {@code mCircleFillPaint} before the {@code Paint} instance is created.
   */
  private int mCircleFillColor = DEFAULT_CIRCLE_FILL_COLOR;

  /**
   * Holds the color value for {@code mCircleProgressPaint} before the {@code Paint} instance is created.
   */
  private int mCircleProgressColor = DEFAULT_CIRCLE_PROGRESS_COLOR;

  /**
   * Holds the alpha value for {@code mPointerHaloPaint}.
   */
  private int mPointerAlpha = DEFAULT_POINTER_ALPHA;

  /**
   * Holds the OnTouch alpha value for {@code mPointerHaloPaint}.
   */
  private int mPointerAlphaOnTouch = DEFAULT_POINTER_ALPHA_ONTOUCH;

  /**
   * Distance (in degrees) that the the circle/semi-circle makes up.
   * This amount represents the max of the circle in degrees.
   */
  private float mTotalCircleDegrees;

  /**
   * Distance (in degrees) that the current progress makes up in the circle.
   */
  private float mProgressDegrees;

  /**
   * {@code Path} used to draw the circle/semi-circle.
   */
  private Path mCirclePath;

  /**
   * {@code Path} used to draw the progress on the circle.
   */
  private Path mCircleProgressPath;

  /**
   * Max value that this CircularSeekBar is representing.
   */
  private int mMax;

  /**
   * Progress value that this CircularSeekBar is representing.
   */
  private int mProgress;

  /**
   * If true, then the user can specify the X and Y radii.
   * If false, then the View itself determines the size of the CircularSeekBar.
   */
  private boolean mCustomRadii;

  /**
   * Maintain a perfect circle (equal x and y radius), regardless of view or custom attributes.
   * The smaller of the two radii will always be used in this case.
   * The default is to be a circle and not an ellipse, due to the behavior of the ellipse.
   */
  private boolean mMaintainEqualCircle;

  /**
   * Once a user has touched the circle, this determines if moving outside the circle is able
   * to change the position of the pointer (and in turn, the progress).
   */
  private boolean mMoveOutsideCircle;

  /**
   * Used for enabling/disabling the lock option for easier hitting of the 0 progress mark.
   */
  private boolean lockEnabled = true;

  /**
   * Used for when the user moves beyond the start of the circle when moving counter clockwise.
   * Makes it easier to hit the 0 progress mark.
   */
  private boolean lockAtStart = true;

  /**
   * Used for when the user moves beyond the end of the circle when moving clockwise.
   * Makes it easier to hit the 100% (max) progress mark.
   */
  private boolean lockAtEnd = false;

  /**
   * When the user is touching the circle on ACTION_DOWN, this is set to true.
   * Used when touching the CircularSeekBar.
   */
  private boolean mUserIsMovingPointer = false;

  /**
   * Represents the clockwise distance from {@code mStartAngle} to the touch angle.
   * Used when touching the CircularSeekBar.
   */
  private float cwDistanceFromStart;

  /**
   * Represents the counter-clockwise distance from {@code mStartAngle} to the touch angle.
   * Used when touching the CircularSeekBar.
   */
  private float ccwDistanceFromStart;

  /**
   * Represents the clockwise distance from {@code mEndAngle} to the touch angle.
   * Used when touching the CircularSeekBar.
   */
  private float cwDistanceFromEnd;

  /**
   * Represents the counter-clockwise distance from {@code mEndAngle} to the touch angle.
   * Used when touching the CircularSeekBar.
   * Currently unused, but kept just in case.
   */
  @SuppressWarnings("unused")
  private float ccwDistanceFromEnd;

  /**
   * The previous touch action value for {@code cwDistanceFromStart}.
   * Used when touching the CircularSeekBar.
   */
  private float lastCWDistanceFromStart;

  /**
   * Represents the clockwise distance from {@code mPointerPosition} to the touch angle.
   * Used when touching the CircularSeekBar.
   */
  private float cwDistanceFromPointer;

  /**
   * Represents the counter-clockwise distance from {@code mPointerPosition} to the touch angle.
   * Used when touching the CircularSeekBar.
   */
  private float ccwDistanceFromPointer;

  /**
   * True if the user is moving clockwise around the circle, false if moving counter-clockwise.
   * Used when touching the CircularSeekBar.
   */
  private boolean mIsMovingCW;

  /**
   * The width of the circle used in the {@code RectF} that is used to draw it.
   * Based on either the View width or the custom X radius.
   */
  private float mCircleWidth;

  /**
   * The height of the circle used in the {@code RectF} that is used to draw it.
   * Based on either the View width or the custom Y radius.
   */
  private float mCircleHeight;

  /**
   * Represents the progress mark on the circle, in geometric degrees.
   * This is not provided by the user; it is calculated;
   */
  private float mPointerPosition;

  /**
   * Pointer position in terms of X and Y coordinates.
   */
  private float[] mPointerPositionXY = new float[2];

  /**
   * Listener.
   */
  private OnCircularSeekBarChangeListener mOnCircularSeekBarChangeListener;

  public CircularSeekBar(Context context) {
    super(context);
    init(null, 0);
  }

  public CircularSeekBar(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(attrs, 0);
  }

  public CircularSeekBar(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init(attrs, defStyle);
  }

  /**
   * Initialize the CircularSeekBar with the attributes from the XML style.
   * Uses the defaults defined at the top of this file when an attribute is not specified by the user.
   *
   * @param attrArray TypedArray containing the attributes.
   */
  private void initAttributes(TypedArray attrArray) {
    mCircleXRadius = attrArray.getFloat(R.styleable.CircularSeekBar_circle_x_radius, DEFAULT_CIRCLE_X_RADIUS) * DPTOPX_SCALE;
    mCircleYRadius = attrArray.getFloat(R.styleable.CircularSeekBar_circle_y_radius, DEFAULT_CIRCLE_Y_RADIUS) * DPTOPX_SCALE;
    mPointerRadius = attrArray.getFloat(R.styleable.CircularSeekBar_pointer_radius, DEFAULT_POINTER_RADIUS) * DPTOPX_SCALE;
    mPointerHaloWidth = attrArray.getFloat(R.styleable.CircularSeekBar_pointer_halo_width, DEFAULT_POINTER_HALO_WIDTH) * DPTOPX_SCALE;
    mPointerHaloBorderWidth = attrArray.getFloat(R.styleable.CircularSeekBar_pointer_halo_border_width, DEFAULT_POINTER_HALO_BORDER_WIDTH) * DPTOPX_SCALE;
    mCircleStrokeWidth = attrArray.getFloat(R.styleable.CircularSeekBar_circle_stroke_width, DEFAULT_CIRCLE_STROKE_WIDTH) * DPTOPX_SCALE;

    String tempColor = attrArray.getString(R.styleable.CircularSeekBar_pointer_color);
    if (tempColor != null) {
      try {
        mPointerColor = Color.parseColor(tempColor);
      } catch (IllegalArgumentException e) {
        mPointerColor = DEFAULT_POINTER_COLOR;
      }
    }

    tempColor = attrArray.getString(R.styleable.CircularSeekBar_pointer_halo_color);
    if (tempColor != null) {
      try {
        mPointerHaloColor = Color.parseColor(tempColor);
      } catch (IllegalArgumentException e) {
        mPointerHaloColor = DEFAULT_POINTER_HALO_COLOR;
      }
    }

    tempColor = attrArray.getString(R.styleable.CircularSeekBar_pointer_halo_color_ontouch);
    if (tempColor != null) {
      try {
        mPointerHaloColorOnTouch = Color.parseColor(tempColor);
      } catch (IllegalArgumentException e) {
        mPointerHaloColorOnTouch = DEFAULT_POINTER_HALO_COLOR_ONTOUCH;
      }
    }

    tempColor = attrArray.getString(R.styleable.CircularSeekBar_circle_color);
    if (tempColor != null) {
      try {
        mCircleColor = Color.parseColor(tempColor);
      } catch (IllegalArgumentException e) {
        mCircleColor = DEFAULT_CIRCLE_COLOR;
      }
    }

    tempColor = attrArray.getString(R.styleable.CircularSeekBar_circle_progress_color);
    if (tempColor != null) {
      try {
        mCircleProgressColor = Color.parseColor(tempColor);
      } catch (IllegalArgumentException e) {
        mCircleProgressColor = DEFAULT_CIRCLE_PROGRESS_COLOR;
      }
    }

    tempColor = attrArray.getString(R.styleable.CircularSeekBar_circle_fill);
    if (tempColor != null) {
      try {
        mCircleFillColor = Color.parseColor(tempColor);
      } catch (IllegalArgumentException e) {
        mCircleFillColor = DEFAULT_CIRCLE_FILL_COLOR;
      }
    }

    mPointerAlpha = Color.alpha(mPointerHaloColor);

    mPointerAlphaOnTouch = attrArray.getInt(R.styleable.CircularSeekBar_pointer_alpha_ontouch, DEFAULT_POINTER_ALPHA_ONTOUCH);
    if (mPointerAlphaOnTouch > 255 || mPointerAlphaOnTouch < 0) {
      mPointerAlphaOnTouch = DEFAULT_POINTER_ALPHA_ONTOUCH;
    }

    mMax = attrArray.getInt(R.styleable.CircularSeekBar_max, DEFAULT_MAX);
    mProgress = attrArray.getInt(R.styleable.CircularSeekBar_progress, DEFAULT_PROGRESS);
    mCustomRadii = attrArray.getBoolean(R.styleable.CircularSeekBar_use_custom_radii, DEFAULT_USE_CUSTOM_RADII);
    mMaintainEqualCircle = attrArray.getBoolean(R.styleable.CircularSeekBar_maintain_equal_circle, DEFAULT_MAINTAIN_EQUAL_CIRCLE);
    mMoveOutsideCircle = attrArray.getBoolean(R.styleable.CircularSeekBar_move_outside_circle, DEFAULT_MOVE_OUTSIDE_CIRCLE);
    lockEnabled = attrArray.getBoolean(R.styleable.CircularSeekBar_lock_enabled, DEFAULT_LOCK_ENABLED);

    // Modulo 360 right now to avoid constant conversion
    mStartAngle = ((360f + (attrArray.getFloat((R.styleable.CircularSeekBar_start_angle), DEFAULT_START_ANGLE) % 360f)) % 360f);
    mEndAngle = ((360f + (attrArray.getFloat((R.styleable.CircularSeekBar_end_angle), DEFAULT_END_ANGLE) % 360f)) % 360f);

    if (mStartAngle == mEndAngle) {
      //mStartAngle = mStartAngle + 1f;
      mEndAngle = mEndAngle - .1f;
    }


  }

  /**
   * Initializes the {@code Paint} objects with the appropriate styles.
   */
  private void initPaints() {
    mCirclePaint = new Paint();
    mCirclePaint.setAntiAlias(true);
    mCirclePaint.setDither(true);
    mCirclePaint.setColor(mCircleColor);
    mCirclePaint.setStrokeWidth(mCircleStrokeWidth);
    mCirclePaint.setStyle(Paint.Style.STROKE);
    mCirclePaint.setStrokeJoin(Paint.Join.ROUND);
    mCirclePaint.setStrokeCap(Paint.Cap.ROUND);

    mCircleFillPaint = new Paint();
    mCircleFillPaint.setAntiAlias(true);
    mCircleFillPaint.setDither(true);
    mCircleFillPaint.setColor(mCircleFillColor);
    mCircleFillPaint.setStyle(Paint.Style.FILL);

    mCircleProgressPaint = new Paint();
    mCircleProgressPaint.setAntiAlias(true);
    mCircleProgressPaint.setDither(true);
    mCircleProgressPaint.setColor(mCircleProgressColor);
    mCircleProgressPaint.setStrokeWidth(mCircleStrokeWidth);
    mCircleProgressPaint.setStyle(Paint.Style.STROKE);
    mCircleProgressPaint.setStrokeJoin(Paint.Join.ROUND);
    mCircleProgressPaint.setStrokeCap(Paint.Cap.ROUND);

    mCircleProgressGlowPaint = new Paint();
    mCircleProgressGlowPaint.set(mCircleProgressPaint);
    mCircleProgressGlowPaint.setMaskFilter(new BlurMaskFilter((5f * DPTOPX_SCALE), BlurMaskFilter.Blur.NORMAL));

    mPointerPaint = new Paint();
    mPointerPaint.setAntiAlias(true);
    mPointerPaint.setDither(true);
    mPointerPaint.setStyle(Paint.Style.FILL);
    mPointerPaint.setColor(mPointerColor);
    mPointerPaint.setStrokeWidth(mPointerRadius);

    mPointerHaloPaint = new Paint();
    mPointerHaloPaint.set(mPointerPaint);
    mPointerHaloPaint.setColor(mPointerHaloColor);
    mPointerHaloPaint.setAlpha(mPointerAlpha);
    mPointerHaloPaint.setStrokeWidth(mPointerRadius + mPointerHaloWidth);

    mPointerHaloBorderPaint = new Paint();
    mPointerHaloBorderPaint.set(mPointerPaint);
    mPointerHaloBorderPaint.setStrokeWidth(mPointerHaloBorderWidth);
    mPointerHaloBorderPaint.setStyle(Paint.Style.STROKE);

  }

  /**
   * Calculates the total degrees between mStartAngle and mEndAngle, and sets mTotalCircleDegrees
   * to this value.
   */
  private void calculateTotalDegrees() {
    mTotalCircleDegrees = (360f - (mStartAngle - mEndAngle)) % 360f; // Length of the entire circle/arc
    if (mTotalCircleDegrees <= 0f) {
      mTotalCircleDegrees = 360f;
    }
  }

  /**
   * Calculate the degrees that the progress represents. Also called the sweep angle.
   * Sets mProgressDegrees to that value.
   */
  private void calculateProgressDegrees() {
    mProgressDegrees = mPointerPosition - mStartAngle; // Verified
    mProgressDegrees = (mProgressDegrees < 0 ? 360f + mProgressDegrees : mProgressDegrees); // Verified
  }

  /**
   * Calculate the pointer position (and the end of the progress arc) in degrees.
   * Sets mPointerPosition to that value.
   */
  private void calculatePointerAngle() {
    float progressPercent = ((float) mProgress / (float) mMax);
    mPointerPosition = (progressPercent * mTotalCircleDegrees) + mStartAngle;
    mPointerPosition = mPointerPosition % 360f;
  }

  private void calculatePointerXYPosition() {
    PathMeasure pm = new PathMeasure(mCircleProgressPath, false);
    boolean returnValue = pm.getPosTan(pm.getLength(), mPointerPositionXY, null);
    if (!returnValue) {
      pm = new PathMeasure(mCirclePath, false);
      returnValue = pm.getPosTan(0, mPointerPositionXY, null);
    }
  }

  /**
   * Initialize the {@code Path} objects with the appropriate values.
   */
  private void initPaths() {
    mCirclePath = new Path();
    mCirclePath.addArc(mCircleRectF, mStartAngle, mTotalCircleDegrees);

    mCircleProgressPath = new Path();
    mCircleProgressPath.addArc(mCircleRectF, mStartAngle, mProgressDegrees);
  }

  /**
   * Initialize the {@code RectF} objects with the appropriate values.
   */
  private void initRects() {
    mCircleRectF.set(-mCircleWidth, -mCircleHeight, mCircleWidth, mCircleHeight);
  }

  @Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.translate(this.getWidth() / 2, this.getHeight() / 2);

    canvas.drawPath(mCirclePath, mCirclePaint);
    canvas.drawPath(mCircleProgressPath, mCircleProgressGlowPaint);
    canvas.drawPath(mCircleProgressPath, mCircleProgressPaint);

    canvas.drawPath(mCirclePath, mCircleFillPaint);

    canvas.drawCircle(mPointerPositionXY[0], mPointerPositionXY[1], mPointerRadius + mPointerHaloWidth, mPointerHaloPaint);
    canvas.drawCircle(mPointerPositionXY[0], mPointerPositionXY[1], mPointerRadius, mPointerPaint);
    if (mUserIsMovingPointer) {
      canvas.drawCircle(mPointerPositionXY[0], mPointerPositionXY[1], mPointerRadius + mPointerHaloWidth + (mPointerHaloBorderWidth / 2f), mPointerHaloBorderPaint);
    }
  }

  /**
   * Get the progress of the CircularSeekBar.
   *
   * @return The progress of the CircularSeekBar.
   */
  public int getProgress() {
    int progress = Math.round((float) mMax * mProgressDegrees / mTotalCircleDegrees);
    return progress;
  }

  /**
   * Set the progress of the CircularSeekBar.
   * If the progress is the same, then any listener will not receive a onProgressChanged event.
   *
   * @param progress The progress to set the CircularSeekBar to.
   */
  public void setProgress(int progress) {
    if (mProgress != progress) {
      mProgress = progress;
      if (mOnCircularSeekBarChangeListener != null) {
        mOnCircularSeekBarChangeListener.onProgressChanged(this, progress, false);
      }

      recalculateAll();
      invalidate();
    }
  }

  private void setProgressBasedOnAngle(float angle) {
    mPointerPosition = angle;
    calculateProgressDegrees();
    mProgress = Math.round((float) mMax * mProgressDegrees / mTotalCircleDegrees);
  }

  private void recalculateAll() {
    calculateTotalDegrees();
    calculatePointerAngle();
    calculateProgressDegrees();

    initRects();

    initPaths();

    calculatePointerXYPosition();
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
    int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
    if (mMaintainEqualCircle) {
      int min = Math.min(width, height);
      setMeasuredDimension(min, min);
    } else {
      setMeasuredDimension(width, height);
    }

    // Set the circle width and height based on the view for the moment
    mCircleHeight = (float) height / 2f - mCircleStrokeWidth - mPointerRadius - (mPointerHaloBorderWidth * 1.5f);
    mCircleWidth = (float) width / 2f - mCircleStrokeWidth - mPointerRadius - (mPointerHaloBorderWidth * 1.5f);

    // If it is not set to use custom
    if (mCustomRadii) {
      // Check to make sure the custom radii are not out of the view. If they are, just use the view values
      if ((mCircleYRadius - mCircleStrokeWidth - mPointerRadius - mPointerHaloBorderWidth) < mCircleHeight) {
        mCircleHeight = mCircleYRadius - mCircleStrokeWidth - mPointerRadius - (mPointerHaloBorderWidth * 1.5f);
      }

      if ((mCircleXRadius - mCircleStrokeWidth - mPointerRadius - mPointerHaloBorderWidth) < mCircleWidth) {
        mCircleWidth = mCircleXRadius - mCircleStrokeWidth - mPointerRadius - (mPointerHaloBorderWidth * 1.5f);
      }
    }

    if (mMaintainEqualCircle) { // Applies regardless of how the values were determined
      float min = Math.min(mCircleHeight, mCircleWidth);
      mCircleHeight = min;
      mCircleWidth = min;
    }

    recalculateAll();
  }

  public boolean isLockEnabled() {
    return lockEnabled;
  }

  public void setLockEnabled(boolean lockEnabled) {
    this.lockEnabled = lockEnabled;
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // Convert coordinates to our internal coordinate system
    float x = event.getX() - getWidth() / 2;
    float y = event.getY() - getHeight() / 2;

    // Get the distance from the center of the circle in terms of x and y
    float distanceX = mCircleRectF.centerX() - x;
    float distanceY = mCircleRectF.centerY() - y;

    // Get the distance from the center of the circle in terms of a radius
    float touchEventRadius = (float) Math.sqrt((Math.pow(distanceX, 2) + Math.pow(distanceY, 2)));

    float minimumTouchTarget = MIN_TOUCH_TARGET_DP * DPTOPX_SCALE; // Convert minimum touch target into px
    float additionalRadius; // Either uses the minimumTouchTarget size or larger if the ring/pointer is larger

    if (mCircleStrokeWidth < minimumTouchTarget) { // If the width is less than the minimumTouchTarget, use the minimumTouchTarget
      additionalRadius = minimumTouchTarget / 2;
    } else {
      additionalRadius = mCircleStrokeWidth / 2; // Otherwise use the width
    }
    float outerRadius = Math.max(mCircleHeight, mCircleWidth) + additionalRadius; // Max outer radius of the circle, including the minimumTouchTarget or wheel width
    float innerRadius = Math.min(mCircleHeight, mCircleWidth) - additionalRadius; // Min inner radius of the circle, including the minimumTouchTarget or wheel width

    if (mPointerRadius < (minimumTouchTarget / 2)) { // If the pointer radius is less than the minimumTouchTarget, use the minimumTouchTarget
      additionalRadius = minimumTouchTarget / 2;
    } else {
      additionalRadius = mPointerRadius; // Otherwise use the radius
    }

    float touchAngle;
    touchAngle = (float) ((java.lang.Math.atan2(y, x) / Math.PI * 180) % 360); // Verified
    touchAngle = (touchAngle < 0 ? 360 + touchAngle : touchAngle); // Verified

    cwDistanceFromStart = touchAngle - mStartAngle; // Verified
    cwDistanceFromStart = (cwDistanceFromStart < 0 ? 360f + cwDistanceFromStart : cwDistanceFromStart); // Verified
    ccwDistanceFromStart = 360f - cwDistanceFromStart; // Verified

    cwDistanceFromEnd = touchAngle - mEndAngle; // Verified
    cwDistanceFromEnd = (cwDistanceFromEnd < 0 ? 360f + cwDistanceFromEnd : cwDistanceFromEnd); // Verified
    ccwDistanceFromEnd = 360f - cwDistanceFromEnd; // Verified

    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        // These are only used for ACTION_DOWN for handling if the pointer was the part that was touched
        float pointerRadiusDegrees = (float) ((mPointerRadius * 180) / (Math.PI * Math.max(mCircleHeight, mCircleWidth)));
        cwDistanceFromPointer = touchAngle - mPointerPosition;
        cwDistanceFromPointer = (cwDistanceFromPointer < 0 ? 360f + cwDistanceFromPointer : cwDistanceFromPointer);
        ccwDistanceFromPointer = 360f - cwDistanceFromPointer;
        // This is for if the first touch is on the actual pointer.
        if (((touchEventRadius >= innerRadius) && (touchEventRadius <= outerRadius)) && ((cwDistanceFromPointer <= pointerRadiusDegrees) || (ccwDistanceFromPointer <= pointerRadiusDegrees))) {
          setProgressBasedOnAngle(mPointerPosition);
          lastCWDistanceFromStart = cwDistanceFromStart;
          mIsMovingCW = true;
          mPointerHaloPaint.setAlpha(mPointerAlphaOnTouch);
          mPointerHaloPaint.setColor(mPointerHaloColorOnTouch);
          recalculateAll();
          invalidate();
          if (mOnCircularSeekBarChangeListener != null) {
            mOnCircularSeekBarChangeListener.onStartTrackingTouch(this);
          }
          mUserIsMovingPointer = true;
          lockAtEnd = false;
          lockAtStart = false;
        } else if (cwDistanceFromStart > mTotalCircleDegrees) { // If the user is touching outside of the start AND end
          mUserIsMovingPointer = false;
          return false;
        } else if ((touchEventRadius >= innerRadius) && (touchEventRadius <= outerRadius)) { // If the user is touching near the circle
          setProgressBasedOnAngle(touchAngle);
          lastCWDistanceFromStart = cwDistanceFromStart;
          mIsMovingCW = true;
          mPointerHaloPaint.setAlpha(mPointerAlphaOnTouch);
          mPointerHaloPaint.setColor(mPointerHaloColorOnTouch);
          recalculateAll();
          invalidate();
          if (mOnCircularSeekBarChangeListener != null) {
            mOnCircularSeekBarChangeListener.onStartTrackingTouch(this);
            mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, true);
          }
          mUserIsMovingPointer = true;
          lockAtEnd = false;
          lockAtStart = false;
        } else { // If the user is not touching near the circle
          mUserIsMovingPointer = false;
          return false;
        }
        break;
      case MotionEvent.ACTION_MOVE:
        if (mUserIsMovingPointer) {
          if (lastCWDistanceFromStart < cwDistanceFromStart) {
            if ((cwDistanceFromStart - lastCWDistanceFromStart) > 180f && !mIsMovingCW) {
              lockAtStart = true;
              lockAtEnd = false;
            } else {
              mIsMovingCW = true;
            }
          } else {
            if ((lastCWDistanceFromStart - cwDistanceFromStart) > 180f && mIsMovingCW) {
              lockAtEnd = true;
              lockAtStart = false;
            } else {
              mIsMovingCW = false;
            }
          }

          if (lockAtStart && mIsMovingCW) {
            lockAtStart = false;
          }
          if (lockAtEnd && !mIsMovingCW) {
            lockAtEnd = false;
          }
          if (lockAtStart && !mIsMovingCW && (ccwDistanceFromStart > 90)) {
            lockAtStart = false;
          }
          if (lockAtEnd && mIsMovingCW && (cwDistanceFromEnd > 90)) {
            lockAtEnd = false;
          }
          // Fix for passing the end of a semi-circle quickly
          if (!lockAtEnd && cwDistanceFromStart > mTotalCircleDegrees && mIsMovingCW && lastCWDistanceFromStart < mTotalCircleDegrees) {
            lockAtEnd = true;
          }

          if (lockAtStart && lockEnabled) {
            // TODO: Add a check if mProgress is already 0, in which case don't call the listener
            mProgress = 0;
            recalculateAll();
            invalidate();
            if (mOnCircularSeekBarChangeListener != null) {
              mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, true);
            }

          } else if (lockAtEnd && lockEnabled) {
            mProgress = mMax;
            recalculateAll();
            invalidate();
            if (mOnCircularSeekBarChangeListener != null) {
              mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, true);
            }
          } else if ((mMoveOutsideCircle) || (touchEventRadius <= outerRadius)) {
            if (!(cwDistanceFromStart > mTotalCircleDegrees)) {
              setProgressBasedOnAngle(touchAngle);
            }
            recalculateAll();
            invalidate();
            if (mOnCircularSeekBarChangeListener != null) {
              mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, true);
            }
          } else {
            break;
          }

          lastCWDistanceFromStart = cwDistanceFromStart;
        } else {
          return false;
        }
        break;
      case MotionEvent.ACTION_UP:
        mPointerHaloPaint.setAlpha(mPointerAlpha);
        mPointerHaloPaint.setColor(mPointerHaloColor);
        if (mUserIsMovingPointer) {
          mUserIsMovingPointer = false;
          invalidate();
          if (mOnCircularSeekBarChangeListener != null) {
            mOnCircularSeekBarChangeListener.onStopTrackingTouch(this);
          }
        } else {
          return false;
        }
        break;
      case MotionEvent.ACTION_CANCEL: // Used when the parent view intercepts touches for things like scrolling
        mPointerHaloPaint.setAlpha(mPointerAlpha);
        mPointerHaloPaint.setColor(mPointerHaloColor);
        mUserIsMovingPointer = false;
        invalidate();
        break;
    }

    if (event.getAction() == MotionEvent.ACTION_MOVE && getParent() != null) {
      getParent().requestDisallowInterceptTouchEvent(true);
    }

    return true;
  }

  private void init(AttributeSet attrs, int defStyle) {
    final TypedArray attrArray = getContext().obtainStyledAttributes(attrs, R.styleable.CircularSeekBar, defStyle, 0);

    initAttributes(attrArray);

    attrArray.recycle();

    initPaints();
  }

  @Override
  protected Parcelable onSaveInstanceState() {
    Parcelable superState = super.onSaveInstanceState();

    Bundle state = new Bundle();
    state.putParcelable("PARENT", superState);
    state.putInt("MAX", mMax);
    state.putInt("PROGRESS", mProgress);
    state.putInt("mCircleColor", mCircleColor);
    state.putInt("mCircleProgressColor", mCircleProgressColor);
    state.putInt("mPointerColor", mPointerColor);
    state.putInt("mPointerHaloColor", mPointerHaloColor);
    state.putInt("mPointerHaloColorOnTouch", mPointerHaloColorOnTouch);
    state.putInt("mPointerAlpha", mPointerAlpha);
    state.putInt("mPointerAlphaOnTouch", mPointerAlphaOnTouch);
    state.putBoolean("lockEnabled", lockEnabled);

    return state;
  }

  @Override
  protected void onRestoreInstanceState(Parcelable state) {
    Bundle savedState = (Bundle) state;

    Parcelable superState = savedState.getParcelable("PARENT");
    super.onRestoreInstanceState(superState);

    mMax = savedState.getInt("MAX");
    mProgress = savedState.getInt("PROGRESS");
    mCircleColor = savedState.getInt("mCircleColor");
    mCircleProgressColor = savedState.getInt("mCircleProgressColor");
    mPointerColor = savedState.getInt("mPointerColor");
    mPointerHaloColor = savedState.getInt("mPointerHaloColor");
    mPointerHaloColorOnTouch = savedState.getInt("mPointerHaloColorOnTouch");
    mPointerAlpha = savedState.getInt("mPointerAlpha");
    mPointerAlphaOnTouch = savedState.getInt("mPointerAlphaOnTouch");
    lockEnabled = savedState.getBoolean("lockEnabled");

    initPaints();

    recalculateAll();
  }


  public void setOnSeekBarChangeListener(OnCircularSeekBarChangeListener l) {
    mOnCircularSeekBarChangeListener = l;
  }

  /**
   * Gets the circle color.
   *
   * @return An integer color value for the circle
   */
  public int getCircleColor() {
    return mCircleColor;
  }

  /**
   * Sets the circle color.
   *
   * @param color the color of the circle
   */
  public void setCircleColor(int color) {
    mCircleColor = color;
    mCirclePaint.setColor(mCircleColor);
    invalidate();
  }

  /**
   * Gets the circle progress color.
   *
   * @return An integer color value for the circle progress
   */
  public int getCircleProgressColor() {
    return mCircleProgressColor;
  }

  /**
   * Sets the circle progress color.
   *
   * @param color the color of the circle progress
   */
  public void setCircleProgressColor(int color) {
    mCircleProgressColor = color;
    mCircleProgressPaint.setColor(mCircleProgressColor);
    invalidate();
  }

  /**
   * Gets the pointer color.
   *
   * @return An integer color value for the pointer
   */
  public int getPointerColor() {
    return mPointerColor;
  }

  /**
   * Sets the pointer color.
   *
   * @param color the color of the pointer
   */
  public void setPointerColor(int color) {
    mPointerColor = color;
    mPointerPaint.setColor(mPointerColor);
    invalidate();
  }

  /**
   * Gets the pointer halo color.
   *
   * @return An integer color value for the pointer halo
   */
  public int getPointerHaloColor() {
    return mPointerHaloColor;
  }

  /**
   * Sets the pointer halo color.
   *
   * @param color the color of the pointer halo
   */
  public void setPointerHaloColor(int color) {
    mPointerHaloColor = color;
    mPointerHaloPaint.setColor(mPointerHaloColor);
    invalidate();
  }

  /**
   * Gets the pointer alpha value.
   *
   * @return An integer alpha value for the pointer (0..255)
   */
  public int getPointerAlpha() {
    return mPointerAlpha;
  }

  /**
   * Sets the pointer alpha.
   *
   * @param alpha the alpha of the pointer
   */
  public void setPointerAlpha(int alpha) {
    if (alpha >= 0 && alpha <= 255) {
      mPointerAlpha = alpha;
      mPointerHaloPaint.setAlpha(mPointerAlpha);
      invalidate();
    }
  }

  /**
   * Gets the pointer alpha value when touched.
   *
   * @return An integer alpha value for the pointer (0..255) when touched
   */
  public int getPointerAlphaOnTouch() {
    return mPointerAlphaOnTouch;
  }

  /**
   * Sets the pointer alpha when touched.
   *
   * @param alpha the alpha of the pointer (0..255) when touched
   */
  public void setPointerAlphaOnTouch(int alpha) {
    if (alpha >= 0 && alpha <= 255) {
      mPointerAlphaOnTouch = alpha;
    }
  }

  /**
   * Gets the circle fill color.
   *
   * @return An integer color value for the circle fill
   */
  public int getCircleFillColor() {
    return mCircleFillColor;
  }

  /**
   * Sets the circle fill color.
   *
   * @param color the color of the circle fill
   */
  public void setCircleFillColor(int color) {
    mCircleFillColor = color;
    mCircleFillPaint.setColor(mCircleFillColor);
    invalidate();
  }

  /**
   * Get the current max of the CircularSeekBar.
   *
   * @return Synchronized integer value of the max.
   */
  public synchronized int getMax() {
    return mMax;
  }

  /**
   * Set the max of the CircularSeekBar.
   * If the new max is less than the current progress, then the progress will be set to zero.
   * If the progress is changed as a result, then any listener will receive a onProgressChanged event.
   *
   * @param max The new max for the CircularSeekBar.
   */
  public void setMax(int max) {
    if (!(max <= 0)) { // Check to make sure it's greater than zero
      if (max <= mProgress) {
        mProgress = 0; // If the new max is less than current progress, set progress to zero
        if (mOnCircularSeekBarChangeListener != null) {
          mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, false);
        }
      }
      mMax = max;

      recalculateAll();
      invalidate();
    }
  }

  /**
   * Listener for the CircularSeekBar. Implements the same methods as the normal OnSeekBarChangeListener.
   */
  public interface OnCircularSeekBarChangeListener {

    void onProgressChanged(CircularSeekBar circularSeekBar, int progress, boolean fromUser);

    void onStopTrackingTouch(CircularSeekBar seekBar);

    void onStartTrackingTouch(CircularSeekBar seekBar);
  }

}